/*
 * Copyright 2010 Askar Kalykov, oxygene.core@gmail.com
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.oxygenecore.processor.description;

import com.oxygenecore.processor.ExecutionContext;
import com.oxygenecore.processor.Expression;
import com.oxygenecore.processor.LeafExpression;
import com.oxygenecore.processor.Statement;
import com.oxygenecore.processor.description.impl.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @author Askar Kalykov
 *         Date: 15.09.2010
 *         Time: 22:41:44
 */
//todo reassemble scanner
public class CalculationClassScanner {
    private ClassLoader classLoader;
    private ClassScannerListener listener;

    private List<Source> sources = new ArrayList<Source>();

    public CalculationClassScanner(ClassLoader classLoader, ClassScannerListener listener){
        if (classLoader == null) {
            throw new NullPointerException("classLoader");
        }
        if (listener == null) {
            throw new NullPointerException("listener");
        }
        this.classLoader = classLoader;
        this.listener = listener;
    }

    public void addSource(File root, File file) throws IOException {
        if (root == null){
            throw new NullPointerException("root");
        }
        if (file == null){
            throw new NullPointerException("file");
        }
        if (file.isFile() && !file.getName().endsWith(".class")){
            throw new IllegalArgumentException("file must be directory or .class file");
        }
        sources.add(new Source(root, file));
    }

    public void traverse() throws IOException {
        listener.beginTraverse();
        for (Source source : sources) {
            if (source.isDirectory()){
                traverseDirectory(source);
            } else {
                traverseFile(source);
            }
        }
        listener.endTraverse();
    }

    private void traverseDirectory(Source directorySource) throws IOException {
        if (directorySource.isDirectory()){
            for (Source source : directorySource) {
                if (source.isDirectory()){
                    traverseDirectory(source);
                } else {
                    if (source.getPath().endsWith(".class")){
                        traverseFile(source);
                    }
                }
            }
        } else {
            throw new IllegalArgumentException("directorySource must point to directory");
        }
    }

    private void traverseFile(Source source) {
        String fullname = grabFullClassName(source);
        traverseClass(fullname);
    }



    private void traverseClass(String fullname) {
        try {
            Class<?> aClass = Class.forName(fullname, true, classLoader);
            if ((aClass.getModifiers() & Modifier.ABSTRACT) != 0){
                return;
            }



            if (Expression.class.isAssignableFrom(aClass)){
                ExpressionClassInfoImpl info = new ExpressionClassInfoImpl(
                        aClass.getName(),
                        "",
                        aClass,
                        getExpressionType((Class<Expression>) aClass),
                        LeafExpression.class.isAssignableFrom(aClass));

                listener.beginExpression(info);
                traverseExpression(aClass, info);
                listener.endExpression();

            } else if (Statement.class.isAssignableFrom(aClass)){

                StatementClassInfoImpl info = new StatementClassInfoImpl(aClass.getName(), "", aClass);

                listener.beginStatement(info);
                traverseStatement(aClass, info);
                listener.endStatement();

            }
        } catch (ClassNotFoundException e) {
            System.err.println("Can't load class " + fullname + ": " + e);
        }
    }

    //todo correct generic parameters handling; leafValue wrong parsed
    private void traverseStatement(Class<?> aClass, StatementClassInfoImpl statementClassInfo) {
        Constructor<?>[] constructors = aClass.getConstructors();

        for (Constructor<?> constructor : constructors) {

            ConstructorInfoImpl constructorInfo = new ConstructorInfoImpl(statementClassInfo, "<init>", "", constructor);
            statementClassInfo.addConstructorInfo(constructorInfo);

            listener.beginConstructor(constructorInfo);

            Type[] types = constructor.getGenericParameterTypes();
            Class<?>[] classes = constructor.getParameterTypes();

            for (int i = 0; i < types.length; i++){
//                System.out.println("type[i]: " + types[i]);
                Class parameterClass = classes[i];
//                System.out.println("parameterClass: " + parameterClass);

                if (Statement.class.isAssignableFrom(parameterClass)){
                    StatementParameterInfoImpl statementParameterInfo = new StatementParameterInfoImpl(constructorInfo, "" + i, "", i);
                    constructorInfo.addParameterInfo(statementParameterInfo);
                    listener.beginStatementParameter(statementParameterInfo);
                    listener.endStatementParameter();
                    continue;
                }


                if (List.class.isAssignableFrom(parameterClass)){
                    ParameterizedType parameterType = (ParameterizedType) types[i];
                    Type listItemType = parameterType.getActualTypeArguments()[0];

                    if (listItemType == Statement.class){
                        StatementParameterInfoImpl statementInfo = new StatementParameterInfoImpl(constructorInfo, "listItem", "", 0);
                        ListParameterInfoImpl listParameterInfo = new ListParameterInfoImpl(constructorInfo, "" + i, "", i, statementInfo);
                        constructorInfo.addParameterInfo(listParameterInfo);
                        listener.beginListParameterInfo(listParameterInfo);
                        listener.endListParameterInfo();
                        continue;
                    }

                    if (listItemType instanceof ParameterizedType){
                        Class listItemType1;
                        Class exprType = Object.class;
                        if (listItemType instanceof Class){
                            listItemType1 = (Class) listItemType;
                        } else if (listItemType instanceof ParameterizedType){
                            ParameterizedType listItemParameterizedType = (ParameterizedType) listItemType;
                            exprType = (Class) listItemParameterizedType.getActualTypeArguments()[0];//?
                            listItemType1 = (Class) listItemParameterizedType.getRawType();
                        } else {
                            throw new IllegalStateException("wtf?");
                        }
                        if (Expression.class.isAssignableFrom(listItemType1)) {
                            boolean leaf = LeafExpression.class.isAssignableFrom(listItemType1);

                            ExpressionParameterInfoImpl expressionParameterInfo = new ExpressionParameterInfoImpl(constructorInfo, "listItem", "", 0, exprType, leaf);
                            ListParameterInfoImpl listParameterInfo =
                                    new ListParameterInfoImpl(constructorInfo, "" + i, "", i, expressionParameterInfo);
                            constructorInfo.addParameterInfo(listParameterInfo);

                            listener.beginListParameterInfo(listParameterInfo);
                            listener.endListParameterInfo();
                            continue;
                        } else {
                            throw new IllegalArgumentException("expression should accept Expression<T> or List<Expression<T>> as a parameter");
                        }
                    } else {
                        throw new IllegalArgumentException("expression should accept Expression<T> or List<Expression<T>> as a parameter");
                    }
                }

                if (Expression.class.isAssignableFrom(parameterClass)){
                    Type actualTypeArgument;
                    if (types[i] instanceof Class){
                        actualTypeArgument = (Type) types[i];
                    } else if (types[i] instanceof ParameterizedType){
                        ParameterizedType parameterType = (ParameterizedType) types[i];
                        actualTypeArgument = parameterType.getActualTypeArguments()[0];
                    } else {
                        throw new IllegalStateException("wtf?");
                    }

//                    System.out.println("actualTypeArgument: " + actualTypeArgument + " of type " + actualTypeArgument.getClass());
                    if (actualTypeArgument instanceof ParameterizedType){
                        actualTypeArgument = ((ParameterizedType)actualTypeArgument).getRawType();
                    } else if (actualTypeArgument instanceof TypeVariable){
                        actualTypeArgument = ((TypeVariable)actualTypeArgument).getBounds()[0];
                    }
                    ExpressionParameterInfoImpl expressionParameterInfo = new ExpressionParameterInfoImpl(constructorInfo, "" + i, "", i,
                            (Class) actualTypeArgument, false);
                    constructorInfo.addParameterInfo(expressionParameterInfo);
                    listener.beginExpressionParameter(expressionParameterInfo);
                    listener.endExpressionParameter();
                    continue;
                }

            }

            listener.endConstructor();
        }

    }

    private Class getExpressionType(Class<? extends Expression> expressionClass){
        try {
            Method method = expressionClass.getMethod("getValue", new Class[]{ExecutionContext.class});// when refactoring Expression.getValue(), don't forget this! 
//            Type type = method.getGenericReturnType();
//            return (Class)type;
            return method.getReturnType();
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
    

    private void traverseExpression(Class aClass, ExpressionClassInfoImpl expressionClassInfo) {
        Constructor<?>[] constructors = aClass.getConstructors();

        boolean leaf = expressionClassInfo.isLeaf();

        if (leaf){
            if (constructors.length != 1 || constructors[0].getParameterTypes().length != 1 || !expressionClassInfo.getType().isAssignableFrom(constructors[0].getParameterTypes()[0])){
                throw new IllegalArgumentException("leaf expression should have one and only one constructor with single argument of expression type");
            }
            ConstructorInfoImpl constructorInfo = new ConstructorInfoImpl(expressionClassInfo, "<init>", "", constructors[0]);
            expressionClassInfo.addConstructorInfo(constructorInfo);
            listener.beginConstructor(constructorInfo);
            ExpressionParameterInfoImpl expressionParameterInfo = new ExpressionParameterInfoImpl(constructorInfo, "0", "", 0, expressionClassInfo.getType(), true);
            listener.beginExpressionParameter(expressionParameterInfo);
            listener.endExpressionParameter();
            listener.endConstructor();
            return;
        }

        for (Constructor<?> constructor : constructors) {

            ConstructorInfoImpl constructorInfo = new ConstructorInfoImpl(expressionClassInfo, "<init>", "", constructor);
            expressionClassInfo.addConstructorInfo(constructorInfo);

            listener.beginConstructor(constructorInfo);

            Type[] types = constructor.getGenericParameterTypes();
            Class<?>[] classes = constructor.getParameterTypes();

            for (int i = 0; i < types.length; i++){
                ParameterizedType parameterType = (ParameterizedType) types[i];
                Class parameterClass = classes[i];

                if (List.class.isAssignableFrom(parameterClass)){
                    Type listItemType = parameterType.getActualTypeArguments()[0];

                    if (listItemType == Statement.class){
                        throw new IllegalStateException("expression should accept Expression<T> or List<Expression<T>> as a parameter");
                    }

                    if (listItemType instanceof ParameterizedType){
                        ParameterizedType listItemParameterizedType = (ParameterizedType) listItemType;
                        if (listItemParameterizedType.getRawType() == Expression.class) {
                            ExpressionParameterInfoImpl expressionParameterInfo = 
                                    new ExpressionParameterInfoImpl(constructorInfo, "", "", 0, Object.class, listItemParameterizedType.getRawType() == LeafExpression.class);
                            ListParameterInfoImpl listParameterInfo =
                                    new ListParameterInfoImpl(constructorInfo, "" + i, "", i, expressionParameterInfo);
                            constructorInfo.addParameterInfo(listParameterInfo);

                            listener.beginListParameterInfo(listParameterInfo);
                            listener.endListParameterInfo();
                            continue;
                        } else {
                            throw new IllegalArgumentException("expression should accept Expression<T> or List<Expression<T>> as a parameter");
                        }
                    } else {
                        throw new IllegalArgumentException("expression should accept Expression<T> or List<Expression<T>> as a parameter");
                    }
                }

                if (Expression.class.isAssignableFrom(parameterClass)){
                    ExpressionParameterInfoImpl expressionParameterInfo = new ExpressionParameterInfoImpl(constructorInfo, "" + i, "", i,
                            (Class)parameterType.getActualTypeArguments()[0], false);
                    constructorInfo.addParameterInfo(expressionParameterInfo);
                    listener.beginExpressionParameter(expressionParameterInfo);
                    listener.endExpressionParameter();
                    continue;
                }
            }

            listener.endConstructor();
        }
    }

    private String grabFullClassName(Source source) {
        String path = source.getPath();
        while (path.startsWith("/") || path.startsWith("\\")){
            path = path.substring(1);
        }
        if (path.endsWith(".class")){
            path = path.substring(0, path.length()-6);
        }
        return path.replace("/",".").replace("\\",".");
    }

    private class Source implements Iterable<Source>{

        private String path;
        private File root;
        private File file;
        private boolean directory;

        private Source(File root, File file) throws IOException {
            if (root == null){
                throw new NullPointerException("root");
            }
            if (file == null){
                throw new NullPointerException("file");
            }
            if (!root.exists()){
                throw new FileNotFoundException(root.getCanonicalPath());
            }
            if (!file.exists()){
                throw new FileNotFoundException(file.getCanonicalPath());
            }
            this.root = root;
            this.file = file;
            String rootPath = root.getCanonicalPath();
            String classFilePath = file.getCanonicalPath();
            if(!classFilePath.startsWith(rootPath)){
                throw new IllegalStateException();
            }
            path = classFilePath.substring(rootPath.length());
            directory = file.isDirectory();
        }

        private String getPath(){
            return path;
        }

        public boolean isDirectory() {
            return directory;
        }

        public Iterator<Source> iterator() {
            File[] children = file.listFiles();
            List<Source> list = new ArrayList<Source>();
            if (children != null){
                for (File child : children) {
                    try{
                        list.add(new Source(root, child));
                    } catch (IOException e){
                        throw new IllegalStateException(e);
                    }
                }
            }

            return list.iterator();
        }
    }
}
